package com.shisan.tingshu.order.adapter.impl;

import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper;
import com.shisan.tingshu.account.client.UserAccountFeignClient;
import com.shisan.tingshu.common.execption.ShisanException;
import com.shisan.tingshu.common.rabbit.constant.MqConst;
import com.shisan.tingshu.common.rabbit.service.RabbitService;
import com.shisan.tingshu.common.result.Result;
import com.shisan.tingshu.common.result.ResultCodeEnum;
import com.shisan.tingshu.model.order.LocalMsg;
import com.shisan.tingshu.order.adapter.PayWay;
import com.shisan.tingshu.order.mapper.LocalMsgMapper;
import com.shisan.tingshu.order.service.OrderInfoService;
import com.shisan.tingshu.vo.account.AccountLockResultVo;
import com.shisan.tingshu.vo.account.AccountLockVo;
import com.shisan.tingshu.vo.order.OrderInfoVo;
import io.seata.spring.annotation.GlobalTransactional;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.data.redis.core.StringRedisTemplate;
import org.springframework.scheduling.annotation.Scheduled;
import org.springframework.stereotype.Service;
import org.springframework.util.StringUtils;

import java.util.List;

/**
 * 零钱支付逻辑
 */
@Service
public class RechargePayWayImpl implements PayWay {


    @Autowired
    private OrderInfoService orderInfoService;

    @Autowired
    private UserAccountFeignClient userAccountFeignClient;

    @Autowired
    private RabbitService rabbitService;

    @Autowired
    private LocalMsgMapper localMsgMapper;

    @Autowired
    private StringRedisTemplate redisTemplate;


    String unLockCacheKey = "";

    String minusCacheKey = "";


    /**
     * 定时任务。让“发消息给解锁队列”这个操作一直执行。只有等下游回复解锁成功后，才停止
     * @param
     * @return
     */
    @Scheduled(fixedDelay = 1000 * 60)
    public void scanLocalMsgAndRetryMsg() {

        // 1.查询本地消息表
        LambdaQueryWrapper<LocalMsg> wrapper = new LambdaQueryWrapper<>();
        wrapper.eq(LocalMsg::getStatus, 0); // 取出状态为0的消息
        // 2.获取消息没有修改的订单编号
        List<LocalMsg> localMsgs = localMsgMapper.selectList(wrapper);
        for (LocalMsg localMsg : localMsgs) {
            // 3.发送检索消息状态是0的消息 选择发送
            String unLockStr = redisTemplate.opsForValue().get(unLockCacheKey);
            if (!StringUtils.isEmpty(unLockStr)) {
                rabbitService.sendMessage(MqConst.EXCHANGE_ACCOUNT, MqConst.ROUTING_ACCOUNT_UNLOCK, localMsg.getMsgContent());
            }
            String minusStr = redisTemplate.opsForValue().get(minusCacheKey);
            if (!StringUtils.isEmpty(minusStr)) {
                rabbitService.sendMessage(MqConst.EXCHANGE_ACCOUNT, MqConst.ROUTING_ACCOUNT_MINUS, localMsg.getMsgContent());
            }
        }
    }

    @Override
    public boolean isSupport(String payWay) {

        return "1103".equals(payWay);
    }

    /**
     * 分布式事务产生的原因及解决：
     * 1、若是RPC远程调用导致的分布式事务，解决1：异常机制+反向操作（反向rpc或反向发消息）；解决2：使用Seata的分布式事务框架
     * 2、若是消息队列导致分布式事务，解决：本地消息表+重试机制（比如定时任务的重试）
     * <p>
     * 1.使用OpenFeign的时候，由于重试机制，所以会导致接口被重复调用，因此一定要解决由于OpenFeign重试机制的幂等性
     * 2.使用消息队列的时候，由于发送方为了保证消息百分百发送成功，也会引入重试机制（定时任务的重试）。那么也会导致下游消费者会重复消费消息，所以也一定要控制幂等性
     *
     * 零钱支付方式的处理支付逻辑
     * @param orderInfoVo
     * @param userId
     * @param orderNo
     */
    @GlobalTransactional
    @Override
    public void dealPayWay(OrderInfoVo orderInfoVo, Long userId, String orderNo) {
        // 零钱支付逻辑

        // 1.检查零钱是否充足，并锁定余额
        // 如果零钱充足，才保存订单相关信息
        // 如果零钱不充足，不用保存订单相关信息
        // 远程调用账户微服务查询并锁定余额，此处只能用RPC，不能用消息队列
        AccountLockVo accountLockVo = prePareUserAccountVo(orderNo, userId, orderInfoVo);
        Result<AccountLockResultVo> lockResultVo = userAccountFeignClient.checkAndLockAmount(accountLockVo);
        if (lockResultVo.getCode() != 200) {
            throw new ShisanException(ResultCodeEnum.ACCOUNT_LOCK_ERROR);
        }
        // 2.保存订单信息
        try {
            // 2.1 初始化本地消息表
            initLocalMsg(orderNo);
            // 2.2 保存订单信息
            orderInfoService.saveOrderInfo(orderInfoVo, userId, orderNo);
            // 3.解锁以及真正地扣减余额（tingshu_account表的total_amount字段和lock_amount字段），使用同步RPC或者异步消息队列都可以
            rabbitService.sendMessage(MqConst.EXCHANGE_ACCOUNT, MqConst.ROUTING_ACCOUNT_MINUS, orderNo);
        } catch (ShisanException e) { // 捕获`2.2 保存订单信息`抛出的异常，即如果保存订单信息失败
            // 反向解锁“锁定金额返回对象”
            rabbitService.sendMessage(MqConst.EXCHANGE_ACCOUNT, MqConst.ROUTING_ACCOUNT_UNLOCK, orderNo); // 这一行代码不一定能够执行成功，所以需要使用本地消息表+定时任务重试机制
            unLockCacheKey = "unlock:fail:flag" + orderNo; // 在Redis中存储一个key，当定时任务执行的时候，会去查询这个key是否存在，如果存在，就继续执行解锁操作
            redisTemplate.opsForValue().set(unLockCacheKey, "1");
            // 4.处理【零钱】支付成功之后的事情
            orderInfoService.PaySuccess(userId, orderNo);
            throw new ShisanException(201, "反向解锁“锁定金额返回对象”失败");
        } catch (Exception e) { // 捕获` 3.解锁以及真正地扣减余额`抛出的异常
            // 反向修改tingshu_account表的total_amount字段和lock_amount字段，使用同步RPC或者异步消息队列都可以
            rabbitService.sendMessage(MqConst.EXCHANGE_ACCOUNT, MqConst.ROUTING_ACCOUNT_MINUS, orderNo); // 这一行代码不一定能够执行成功，所以需要使用本地消息表+定时任务重试机制
            minusCacheKey = "minus:fail:flag" + orderNo; // 在Redis中存储一个key，当定时任务执行的时候，会去查询这个key是否存在，如果存在，就继续执行消费操作
            redisTemplate.opsForValue().set(minusCacheKey, "1");
            throw new ShisanException(201, "反向扣减余额失败");
        }
    }

    /**
     * 初始化本地消息表
     * @param orderNo
     */
    public void initLocalMsg(String orderNo) {
        LocalMsg localMsg = new LocalMsg();
        localMsg.setMsgContent(orderNo);
        localMsg.setStatus(0);
        localMsgMapper.insert(localMsg);
    }

    /**
     * 准备用户账户信息
     * @param orderNo
     * @param userId
     * @param orderInfoVo
     * @return
     */
    private AccountLockVo prePareUserAccountVo(String orderNo, Long userId, OrderInfoVo orderInfoVo) {

        AccountLockVo accountLockVo = new AccountLockVo();
        accountLockVo.setOrderNo(orderNo);
        accountLockVo.setUserId(userId);
        accountLockVo.setAmount(orderInfoVo.getOrderAmount());// 实际买商品要花的钱
        accountLockVo.setContent(orderInfoVo.getOrderDetailVoList().get(0).getItemName());

        return accountLockVo;
    }
}
